package hr.fer.pus.dll_will.sp;

import hr.fer.pus.dll_will.cr.data.FileDescriptor;
import hr.fer.pus.dll_will.sherlock.Security;
import hr.fer.pus.dll_will.sp.data.ServiceFile;
import hr.fer.pus.dll_will.sp.data.User;
import hr.fer.pus.dll_will.sp.data.XMLMiner;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * On-line document provider.
 */
public class ServiceProvider extends HttpServlet {
	private static final long serialVersionUID = 1L;
	
	/** Users: K:username, V:user */
	private Map<String, User> users;
	
	/** Files: K:filename, V:file */
	private Map<String, ServiceFile> files;
	
	/** Descriptor of all files got from CR. */
	private Map<String, FileDescriptor> allFiles;
	
	/** Cached public keys of other SPs. K:SP ID, V:Public Key */
	private Map<String, String> cachedPubKeys;
	
	/** URLs of other SPs. K:SP ID, V:URL */
	private Map<String, String> spURLs;
	
	/** Name of this SP. */
	private String spName;
	
	/** ID of this SP. */
	private String spID = "42";
	
	/** Base URL of CR. */
	private String crBaseUrl;
	
	/** CRs public key. */
	private String crPublicKey;
	
	/** Public key of this SP. */
	private String publicKey;
	
	/** Private key of this SP. */
	private String privateKey;
	
	/**
	 * Digital certificate of this SP signed by CR.
	 * Contains SPs ID and pub. key.
	 */
	private String digitalCertificate;
	
	/** Address of this SP. */
	private String address;
	
	/**
	 * HTTP GET request processing.
	 */
	@Override
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {

		processRequest(request, response);
	}

	/**
	 * HTTP POST request processing.
	 */
	@Override
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {

		processRequest(request, response);
	}

	/**
	 * Request processing.
	 * 
	 * @param request HTTP Request with parameters.
	 * @param response HTTP Response for forwaring of request.
	 * @throws IOException If read/write error occurs.
	 */
	private void processRequest(HttpServletRequest request,
			HttpServletResponse response) throws IOException {

		String action = (String) request.getParameter("action");
		if (action == null) action = "error";
		action = action.toLowerCase();
		request.setAttribute("page", action);
		
		if (action.equals("read")) {
			String filename = request.getParameter("filename");
			if (isLogged(request, response)) {
				read(filename, request, response);
			} else {
				writeError("Not logged in!", request, response);
			}
		}
		else if (action.equals("listfiles")) {
			if (isLogged(request, response)) {
				listFiles(request, response);
			} else {
				writeError("Not logged in!", request, response);
			}
		}
		else if (action.equals("login")) {
			String username = (String) request.getParameter("username");
			String password = (String) request.getParameter("password");
			
			System.out.println("loging in: " + username);
			
			
			if(login(username, password)) {
				System.out.println("success!");
				HttpSession session = request.getSession(true);
				session.setAttribute("logged", Boolean.valueOf(true));
				session.setAttribute("username", username);
				listFiles(request, response);
			} else {
				writeError("Invalid username or password!", request, response);
			}
		}
		else if (action.equals("remoteread")) {
			remoteWrite(request, response);
		
		} else if (action.equals("certificate")) {
			exchangeCertificate(request, response);
			
		} else {
			writeError("Invalid page!", request, response);
		}
	}
	
	/**
	 * Exchange certificate with other SP.
	 * 
	 * @param request
	 * @param response
	 * @throws IOException
	 */
	private void exchangeCertificate(HttpServletRequest request, HttpServletResponse response) throws IOException {
		String own = (String) request.getAttribute("own");
		String spId = (String) request.getAttribute("id");
		
		String[] cert = Security.decrypt(own, crPublicKey).split(" ");
		if (!cert[0].equals(spId)) return; // invalid req.
		
		cachedPubKeys.put(spId, cert[1]+" "+cert[2]);
		
		ServletOutputStream out = response.getOutputStream();
		response.setContentType("text/plain");
		out.write(digitalCertificate.getBytes("UTF-8"));
		out.flush();
		out.close();
	}
	
	/**
	 * Return error page with message.
	 * 
	 * @param msg Error message.
	 * @param request
	 * @param response
	 */
	private void writeError(String msg, HttpServletRequest request, HttpServletResponse response) {
		try {
			msg = URLEncoder.encode(msg, "UTF-8");
			request.setAttribute("msg", msg);
			this.getServletContext().getRequestDispatcher(
				"/WEB-INF/error.jsp").forward(request, response);
		} catch (ServletException e) {
			e.printStackTrace();
			try {
				e.printStackTrace(new PrintStream(new File("error.txt")));
			} catch (FileNotFoundException e1) {
				e1.printStackTrace();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}

	/**
	 * Is user logged in.
	 * @param request
	 * @param response
	 * @return True if user is logged in.
	 */
	private boolean isLogged(HttpServletRequest request, HttpServletResponse response) {
		HttpSession session = request.getSession(true);
		Boolean logged = (Boolean) session.getAttribute("logged");
		if (logged == null || !logged.booleanValue()) {
			return false;
		}
		return true;
	}
	
	/**
	 * List all available files.
	 * 
	 * @param request
	 * @param response
	 */
	private void listFiles(HttpServletRequest request, HttpServletResponse response) {
		this.allFiles = getAllFilesFromCR(); // Every time instead only at init...
		request.setAttribute("files", this.allFiles.keySet());
		
		try {
			this.getServletContext().getRequestDispatcher(
					"/WEB-INF/list.jsp").forward(request, response);
		} catch (ServletException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
	}
	
	/**
	 * Read file (localy or remotely) and return its content.
	 * @param filename
	 * @param request
	 * @throws IOException 
	 */
	private void read(String filename, HttpServletRequest request, HttpServletResponse response) throws IOException {
		String content = null;
		if (files.containsKey(filename)) {
			content = files.get(filename).getContent();
		} else if (this.allFiles.containsKey(filename)) {
			String username = (String) request.getSession().getAttribute("username");
			content = remoteRead(username, filename);
		} else {
			this.allFiles = getAllFilesFromCR();
			if (this.allFiles.containsKey(filename)) {
				String username = (String) request.getSession().getAttribute("username");
				content = remoteRead(username, filename);
			}
		}
		
		if (content == null) {
			writeError("File " + filename + " does not exist!", request, response);
			return;
		}
		
		content = content.replaceAll("\n", "<br />");
		try {
			content = URLEncoder.encode(content, "UTF-8");
			filename = URLEncoder.encode(filename, "UTF-8");
		} catch (UnsupportedEncodingException e1) {
			e1.printStackTrace();
		}
		
		request.setAttribute("content", content);
		request.setAttribute("filename", filename);
		try {
			this.getServletContext().getRequestDispatcher(
					"/WEB-INF/show.jsp").forward(request, response);
		} catch (ServletException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Response to remote read request.
	 * @param request
	 * @param response
	 * @throws IOException 
	 * @throws UnsupportedEncodingException 
	 */
	private void remoteWrite(HttpServletRequest request,
			HttpServletResponse response) throws UnsupportedEncodingException, IOException {
		
		ServletOutputStream out = response.getOutputStream();
		response.setContentType("text/plain");
		
		String readReq = (String) request.getAttribute("readrequest");
		if (readReq == null) {
			out.write("Invalid request!".getBytes("UTF-8"));
			out.flush();
			out.close();
			return;
		}
		
		readReq = URLDecoder.decode(readReq, "UTF-8");
		String[] reqParts = readReq.split("#");
		String spId = reqParts[0];
		
//		String otherSPKey = this.cachedPubKeys.get(spId);;
//		if (otherSPKey == null) {
//			otherSPKey = retrieveSPPubKey(spId);
//		}
		
//		String reqData = Security.decrypt(reqParts[1], otherSPKey);
		String[] splitData = reqParts[1].split("#");
		System.out.println("RW: " + splitData[0] + " " + splitData[1]);
		ServiceFile sf = files.get(splitData[1]);
		System.out.println("Content: " + sf.getContent());
		if (sf != null) {
			String resp = getID() + "#" + splitData[0] + "#" + splitData[1] + "#" + sf.getContent();
//			resp = Security.encrypt(resp, privateKey);
			out.write(resp.getBytes("UTF-8"));
		} else {
			out.write("File does not exist!".getBytes("UTF-8"));
		}
		
		out.flush();
		out.close();
	}
	
	/**
	 * Read file from other SP.
	 * 
	 * @param username
	 * @param filename
	 * @return File content or null if file doesn't exists.
	 * @throws IOException 
	 */
	private String remoteRead(String username, String filename) throws IOException {
		FileDescriptor fd = allFiles.get(filename);
		if (fd == null) return null;
		
//		String otherSpKey = cachedPubKeys.get(fd.getSpID());
//		if (otherSpKey == null) {
//			otherSpKey = retrieveSPPubKey(String.valueOf(fd.getSpID()));
//		}
		
		// Format: this SP ID#crypted(username#filename)
		String cryptData = Security.encrypt(username+"#"+filename, privateKey);
		String readRequest = String.valueOf(getID()) + "#" + cryptData;
		
		String spUrl = spURLs.get(fd.getSpID());
		if (spUrl == null) return null; // Never! This is here for debugging.
		
		Map<String, String> params = new HashMap<String, String>();
		params.put("action", "remoteread");
		params.put("readrequest", URLEncoder.encode(readRequest, "UTF-8"));
		
		try {
			String readResp = RemoteUtils.postToAddress(params, spUrl);
//			readResp = Security.decrypt(readResp, otherSpKey);
			return readResp.split("#")[3];
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return null;
	}

	/**
	 * Retrieve public key of SP with ID {@code spID}. 
	 * @param spID
	 * @return SP-s public key.
	 * @throws UnsupportedEncodingException 
	 */
	private String retrieveSPPubKey(String spID) throws UnsupportedEncodingException {
		Map<String, String> params = new HashMap<String, String>();
		params.put("action", "cetrificate");
		params.put("id", this.spID);
		params.put("own", URLDecoder.decode(digitalCertificate, "UTF-8"));
		
		String spUrl = spURLs.get(spID);
		
		try {
			String reply = RemoteUtils.postToAddress(params, spUrl);
			reply = Security.decrypt(reply, crPublicKey);
			String[] parts = reply.split(" ");
			
			if (spID.equals(parts[0])) {
				String key = parts[1]+" "+parts[2];
				cachedPubKeys.put(spID, key);
				return key;
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return null;
	}

	/**
	 * Check if user data are correct.
	 * 
	 * @param username
	 * @param password
	 * @return True if username and password are vaild.
	 */
	private boolean login(String username, String password) {
		if(users.containsKey(username)) {			
			User user = users.get(username);
			
			if(user.getPassword().equals(password))
				return true;
			
		}
		return false;
	}

	/**
	 * Servlet init. Filling of necessary collections and reading of parameters
	 * from web.xml file.
	 */
	@Override
	public void init(ServletConfig config) throws ServletException {
		super.init(config);
		
		cachedPubKeys = new HashMap<String, String>();
		spURLs = new HashMap<String, String>();
		
		this.spName = config.getInitParameter("name");
		this.address = config.getInitParameter("my_url");
		this.crBaseUrl = config.getInitParameter("cr_url");
		String usersXml = config.getInitParameter("users_xml_path");
		String filesXml = config.getInitParameter("files_xml_path");
		
		this.files = new HashMap<String, ServiceFile>();
		this.users = new HashMap<String, User>();
		String usersFile = getServletContext().getRealPath("/") + File.separator + "WEB-INF" + File.separator + usersXml;
		String filesFile = getServletContext().getRealPath("/") + File.separator + "WEB-INF" + File.separator + filesXml;
		
		File users = new File(usersFile);
		if (!users.exists()) {
			try {
				users.createNewFile();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		Set<User> usersSet = null;
		try {
			usersSet = XMLMiner.getUsers(users);
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		for (User u : usersSet) {
			this.users.put(u.getUsername(), u);
		}
		
		File files = new File(filesFile);
		if (!files.exists()) {
			try {
				files.createNewFile();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		Set<ServiceFile> filesSet = null;
		try {
			filesSet = XMLMiner.getFiles(files);
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		for (ServiceFile f : filesSet) {
			this.files.put(f.getFilename(), f);
		}
		
		try {
			registerSPToCR();
			getCRpublicKey();
			registerFilesToCR(this.files);
		} catch (UnsupportedEncodingException e1) {
			e1.printStackTrace();
		}
		
		this.allFiles = getAllFilesFromCR();
	}

	/**
	 * Get all SPs URL-s and map them.
	 */
	private void mapSPs() {
		try {
			String filesString = RemoteUtils.readFromAddress(crBaseUrl+"?action=listsps");
			filesString = URLDecoder.decode(filesString, "UTF-8");
			String[] sps = filesString.split("#");
			for (int i = 0; i < sps.length; i++) {
				String[] spData = sps[i].split(" ");
				this.spURLs.put(spData[0], spData[1]);
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Retrieve CRs public key.
	 */
	private void getCRpublicKey() {
		try {
			String reply = RemoteUtils.postToAddress(new HashMap<String, String>(), crBaseUrl+"?action=getpublickey");
			this.crPublicKey = reply;
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}

	/**
	 * Retrieve all files registered at CR.
	 * @return
	 */
	private Map<String, FileDescriptor> getAllFilesFromCR() {
		
		Map<String, FileDescriptor> all = new HashMap<String, FileDescriptor>();
		
		try {
			String filesString = RemoteUtils.readFromAddress(crBaseUrl+"?action=listfiles");
			
			fillMap(filesString, all);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		mapSPs();
		
		return all;
	}

	/** 
	 * Fill file descriptors map from unparsed string input. 
	 * @param filesString
	 * @param all
	 * @throws UnsupportedEncodingException 
	 */
	private void fillMap(String filesString, Map<String, FileDescriptor> all) throws UnsupportedEncodingException {
		
		String[] fileList = filesString.split("#");
		for (String file : fileList) {
			String[] params = file.split(" ");
			try{
				Long fileID = Long.valueOf(params[0].trim());
				String fileName = URLDecoder.decode(params[1], "UTF-8");
				String author = params[2];
				String desc = URLDecoder.decode(params[3], "UTF-8");
				Long spID = Long.valueOf(params[4].trim());
				FileDescriptor fds = new FileDescriptor(fileID, fileName, author, desc, spID);
				all.put(fileName, fds);
				
			}catch(NumberFormatException e){
			}
		}
	}

	/**
	 * Register this SP at CR.
	 * 
	 * @throws UnsupportedEncodingException
	 */
	private void registerSPToCR() throws UnsupportedEncodingException {
		SPDescriptor desc = new SPDescriptor(spName, this.getAddress());
		
		Map<String, String> params = new HashMap<String, String>();
		params.put("sp", URLEncoder.encode(desc.toString(), "UTF-8"));
		
		try {
			String reply = RemoteUtils.postToAddress(params, crBaseUrl+"?action=registersp");
			reply = Security.decrypt(reply, crPublicKey);
			String[] parts = reply.split("#");
			String privateExp = Security.decrypt(parts[1], crPublicKey);
			
			this.digitalCertificate = parts[0];
			String[] replyData = parts[0].split(" ");
			this.spID = replyData[0];
			System.out.println("................SPID: " + replyData[0]);
			this.publicKey = replyData[1] + " " + replyData[2];
			this.privateKey = replyData[1] + " " + privateExp;
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Register files owned by this SP to CR.
	 * @param files
	 * @throws UnsupportedEncodingException Never!
	 */
	private void registerFilesToCR(Map<String, ServiceFile> files) throws UnsupportedEncodingException {
		
		System.out.println("starting file registration on SP...");
		
		StringBuilder sb = new StringBuilder();
		String separator = "";
		for (String fileName : files.keySet()) {
			System.out.println("adding file: " + fileName);
			
			ServiceFile file = files.get(fileName);
			String fileID = file.getId() + "";
			String name = URLEncoder.encode(file.getFilename(), "UTF-8");
			String author = file.getOwnerId() + "";
			String description = URLEncoder.encode(file.getDescription(), "UTF-8");
			String spID = this.getID() + "";
			
			sb.append(separator);
			sb.append(fileID);
			sb.append(" ");
			sb.append(name);
			sb.append(" ");
			sb.append(author);
			sb.append(" ");
			sb.append(description);
			sb.append(" ");
			sb.append(spID);
			separator = "#";
		}
		
		Map<String, String> params = new HashMap<String, String>();
		params.put("filelist", sb.toString());
		
		try {
			RemoteUtils.postToAddress(params, crBaseUrl+"?action=registerfiles");
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}

	/**
	 * @return ID of this SP.
	 */
	private String getID() {
		return spID;
	}
	
	/**
	 * @return URL of this SP.
	 */
	private String getAddress() {
		return address;
	}

}
